package czolgisci.map;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;

import javax.imageio.ImageIO;

import utils.time.IClockListener;
import czolgisci.Engine;
import czolgisci.display.ISpritesContainer;
import czolgisci.events.TickEvent;
import czolgisci.tiles.*;
import czolgisci.tiles.factory.ITileFactory;

/**
 * Klasa zarządzająca mapą gry (przesuwa obiekty na mapie, wykrywa kolizje).
 * 
 * Agreguje obiekt klasy ArrayList (używa go jako kontener na obrazki
 * reprezentujące różne typy kafelków, np. czołg lub ścianę).
 * 
 * Oczekuje sygnałów od zegara (implementuje interfejs IClockListener)
 * 
 * @author pillow
 * 
 */
public class MapManager implements ISpritesContainer, IClockListener, ITilesFeedbackListener
{
	private ITileFactory tileKit;
	private ATile[][] tilesContainer;
	private int width, height; //wymiary mapy w pikselach
	private int HorizontalTilesNum, VerticalTilesNum; //wymiary mapy w kafelkach
	private String backgroundPath;
	private ArrayList<AMovableTile> tanks;
	private ArrayList<Point> tanksDefaultPositions;
	private ArrayList<Bullet> bullets;
	private ArrayList<Explosion> explosions;

	/**
	 * Konstruktor klasy
	 * 
	 * @param mapObject
	 *            obiekt mapy
	 * @param tileFactory
	 *            wybrana konkretna fabryka kafelków
	 */
	public MapManager(Map mapObject, ITileFactory tileFactory)
	{
		bullets = new ArrayList<Bullet>();
		explosions = new ArrayList<Explosion>();
		tileKit = tileFactory;
		loadMap(mapObject);
	}
	
	/**
	 * Konstruktor klasy
	 * 
	 * @param tileFactory
	 *            wybrana konkretna fabryka kafelków
	 */
	public MapManager(ITileFactory tileFactory)
	{
		bullets = new ArrayList<Bullet>();
		explosions = new ArrayList<Explosion>();
		tileKit = tileFactory;
	}

	/**
	 * Ładuje mapę
	 * 
	 * @param mapObject
	 *            obiekt mapy
	 */
	public void loadMap(Map mapObject)
	{
		HorizontalTilesNum = mapObject.width;
		VerticalTilesNum = mapObject.height;
		width = HorizontalTilesNum * tileKit.getTileWidth();
		height = VerticalTilesNum * tileKit.getTileHeight();
		backgroundPath = mapObject.backgroundPath;

		tilesContainer = new ATile[VerticalTilesNum][HorizontalTilesNum];
		tanks = new ArrayList<AMovableTile>();
		tanksDefaultPositions = new ArrayList<Point>();
		TileType tileType;
		ATile tile;
		AMovableTile tank;

		for (int j = 0; j < VerticalTilesNum; j++)
		{
			for (int i = 0; i < HorizontalTilesNum; i++)
			{
				try
				{
					tileType = mapObject.getTile(j, i);
					tile = tileKit.makeTile(tileType,
											new Point(i * tileKit.getTileWidth(),
											j * tileKit.getTileHeight()));
					tilesContainer[j][i] = tile;

					if (tileType == TileType.TANK)
					{
						tank = (AMovableTile) tile;
						tanks.add(tank);
						tanksDefaultPositions.add(tank.getPosition());
						tank.registerFeedbackListener(this);
					}
				} catch (IllegalArgumentException e)
				{
					//jeśli znaleziono na mapie nieznany znak
				} catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		}
	}
	
	/**
	 * Niszczy aktualną mapę
	 */
	public void destroyCurrentMap()
	{
		tanks.clear();
		tanksDefaultPositions.clear();
		bullets.clear();
	}

	/**
	 * Pobiera kafelek z pozycji IJ
	 * 
	 * @param i
	 * @param j
	 * @return wskazany kafelek
	 */
	public ATile getTileFromIJ(int i, int j)
	{
		return tilesContainer[j][i];
	}
	
	public ATile getTileIJ(Point pt)
	{
		return tilesContainer[pt.y][pt.x];
	}

	public ATile getTile(Point pt)
	{
		Point pt2 = getIJfromXY(pt.x, pt.y);
		return tilesContainer[pt2.y][pt2.x];
	}
	
	public void setTile(Point pt, ATile tile)
	{
		tilesContainer[pt.y][pt.x] = tile;
	}
	
	/**
	 * @return iterator po kontenerze naboi
	 */
	public Iterator<Bullet> getBulletsIterator()
	{
		return bullets.iterator();
	}

	/**
	 * @return iterator po kontenerze wybuchów
	 */
	public Iterator<Explosion> getExplosionsIterator()
	{
		return explosions.iterator();
	}
	
	/**
	 * @return kontener czołgów
	 */
	public ArrayList<AMovableTile> getTanks()
	{
		return tanks;
	}

	/**
	 * @return ilość kafelków w poziomie
	 */
	public int getVerticalTilesNum()
	{
		return VerticalTilesNum;
	}

	/**
	 * @return ilość kafelków w pionie
	 */
	public int getHorizontalTilesNum()
	{
		return HorizontalTilesNum;
	}

	/**
	 * @return ilość pikseli w poziomie
	 */
	public int getWidth()
	{
		return width;
	}

	/**
	 * @return ilość pikseli w poziomie
	 */
	public int getHeight()
	{
		return height;
	}

	/**
	 * @return obrazek tła dla aktualnej mapy
	 */
	public BufferedImage getBackground() throws IOException
	{
		return (BufferedImage) ImageIO.read(new File(backgroundPath));
	}

	/**
	 * Umożliwia wypełnienie pola mapy obiektem (kafelkiem) z zewnątrz.
	 * 
	 * @param x
	 *            pozycja x (px)
	 * @param y
	 *            pozycja y (px)
	 */
	public void setTileAtXY(int x, int y, ATile tile)
	{
		Point pt = getIJfromXY(x, y);
		tilesContainer[pt.y][pt.x] = tile;
	}

	/**
	 * Sprawdzanie kolizji
	 * 
	 * @see czolgisci.tiles.ITilesFeedbackListener#onTankMove(czolgisci.tiles.Tank)
	 */
	private boolean ifWallIntersects(Rectangle area, int i, int j)
	{
		if(getTileFromIJ(i, j) instanceof Wall)
		{
			if(getTileFromIJ(i, j).getArea().intersects(area))
			{
				return true;
			}
		}
		return false;
	}
	
	private boolean intersectsWithWall(Rectangle area)
	{
		Point pt = getIJfromArea(area);
		int i = pt.x;
		int j = pt.y;
		if(		   ifWallIntersects(area, i, j)
				|| ifWallIntersects(area, i-1, j)
				|| ifWallIntersects(area, i+1, j)
				|| ifWallIntersects(area, i, j+1)
				|| ifWallIntersects(area, i-1, j+1)
				|| ifWallIntersects(area, i+1, j+1)
				|| ifWallIntersects(area, i, j-1)
				|| ifWallIntersects(area, i-1, j-1)
				|| ifWallIntersects(area, i+1, j-1)
			)
		{
			return true;
		}
		return false;
	}
	
	/**
	 * metoda wywoływana przez czołg, gdy ten zmienił swoją pozycję.
	 * Sprawdza kolizje, przesuwa czołg na mapie.
	 * @param tank czołg który się przesunął
	 * @see czolgisci.tiles.ITilesFeedbackListener#onTankMove(czolgisci.tiles.Tank)
	 */
	public void onTankMove(Tank tank)
	{
		Rectangle tankArea = tank.getArea();
		Dimension tankDim = new Dimension(tankArea.width, tankArea.height);
		Point tankPos = tank.getPosition();

		if(intersectsWithWall(tankArea))
		{
			Point pt = tank.getDirection().getReversedDirection().getDirectionPoint();
			
			while(intersectsWithWall(new Rectangle(tankPos, tankDim)))
			{
				tankPos.x += pt.x;
				tankPos.y += pt.y;
			}
			tank.setPosition(tankPos);
			return;
		}
		Point currLocation = getIJfromArea(tankArea);
		Point prevLocation = tank.getDirection().getReversedDirection().getDirectionPoint();
		prevLocation.x += currLocation.x;
		prevLocation.y += currLocation.y;
		ATile tile = getTileFromIJ(currLocation.x, currLocation.y);
		if(tile != tank)
		{
			if(tile instanceof Tank) // zderzenie czołgów
			{
				Tank tank2 = (Tank) tile;
				Engine.getInstance().removePlayerLife(tank.getName());
				Engine.getInstance().removePlayerLife(tank2.getName());
				tank2.destroy();
				tank.destroy();
				setTile(currLocation, null);
				setTile(prevLocation, null);

				try
				{
					registerExplosion((Explosion) tileKit.makeTile(TileType.EXPLOSION,
							new Point((int) tankArea.getCenterX(), (int) tankArea.getCenterY())));
					restartDestroyedTanks();
				} catch (Exception e)
				{
					e.printStackTrace();
				}
				Engine.getInstance().tryEndGame();
			}
			else //zmiana położenia
			{
				setTile(prevLocation, null);
				setTile(currLocation, tank);
			}
		}
	}

	/**
	 * Ustawia czołgi na ich początkowe pozycje
	 */
	private void restartDestroyedTanks() throws Exception
	{
		for (int i = 0; i < tanks.size(); i++)
		{
			Tank tank = (Tank) tanks.get(i);
			if(tank.isDestroyed())
			{
				Point pt = tanksDefaultPositions.get(i);
				tank.setPosition(pt);
				tank.raise();
				if(getTile(pt) instanceof Tank)
				{
					throw new Exception("Czolg pojawil sie na miejscu gdzie juz byl czolg.");
				}
				setTileAtXY(pt.x, pt.y, tank);
			}
		}
	}

	/**
	 * metoda wywoływana przez czołg, gdy ten strzelił.
	 * Sprawdza kolizje, przesuwa nabój na mapie.
	 * @param tank czołg, który wystrzelił
	 * @see czolgisci.tiles.ITilesFeedbackListener#onTankMove(czolgisci.tiles.Tank)
	 */
	public void onShoot(Tank tank)
	{
		try
		{
			Point bullPos = tank.getDirection().getDirectionPoint();
			int bx = (int)tank.getArea().getCenterX() + bullPos.x * tileKit.getTileWidth()/2 ;
			int by = (int)tank.getArea().getCenterY() + bullPos.y * tileKit.getTileHeight()/2;
			Bullet bull = (Bullet) tileKit.makeTile(TileType.BULLET, new Point(bx, by));
			bx -= bull.getSize().width/2;
			by -= bull.getSize().height/2;
			bx += bullPos.x*bull.getSize().width;
			by += bullPos.y*bull.getSize().height;
			bull.setPosition(new Point(bx, by));

			bull.registerFeedbackListener(this);
			bull.setDirection(tank.getDirection());
			registerBullet(bull);
			
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}
	
	/**
	 * metoda wywoływana przez nabój, gdy ten zmienił swoją pozycję.
	 * Sprawdza kolizje, przesuwa nabój na mapie.
	 * @param bullet nabój, który się przesunął
	 * @see czolgisci.tiles.ITilesFeedbackListener#onTankMove(czolgisci.tiles.Tank)
	 */
	public void onBulletMove(Bullet bullet)
	{
		Rectangle bulletArea = bullet.getArea();
		
		Point currLocation = getIJfromArea(bulletArea);
		Point prevLocation = bullet.getDirection().getReversedDirection().getDirectionPoint();
		prevLocation.x += currLocation.x;
		prevLocation.y += currLocation.y;
		
		if(intersectsWithWall(bulletArea))
		{
			destroyBullet(bullet);
			return;
		}
		
		ATile tile = getTileFromIJ(currLocation.x, currLocation.y);
		if(tile instanceof Tank) // zderzenie naboju z czolgiem
		{
			Tank tank = (Tank) tile;
			Engine.getInstance().removePlayerLife(tank.getName());
			tank.destroy();
			destroyBullet(bullet);
			setTile(currLocation, null);
			try
			{
				registerExplosion((Explosion) tileKit.makeTile(TileType.EXPLOSION,
						new Point((int) bulletArea.getCenterX(), (int) bulletArea.getCenterY())));
				restartDestroyedTanks();
			} catch (Exception e)
			{
				e.printStackTrace();
			}
			Engine.getInstance().tryEndGame();
		}
	}
	
	/**
	 * Niszczy nabój
	 * @param bullet
	 */
	private void destroyBullet(Bullet bullet)
	{
		unregisterBullet(bullet);
	}

	/**
	 * Rejestruje nabój w kontenerze
	 * @param bullet
	 */
	private void registerBullet(Bullet bullet)
	{
		if(!bullets.contains(bullet))
			bullets.add(bullet);
	}
	
	/**
	 * Usuwa nabój z kontenera
	 * @param bullet
	 */
	private void unregisterBullet(Bullet bullet)
	{
		bullets.remove(bullet);
	}
	
	/**
	 * Rejestruje nabój w kontenerze
	 * @param bullet
	 */
	private void registerExplosion(Explosion explosion)
	{
		if(!explosions.contains(explosion))
			explosions.add(explosion);
	}
	
	/**
	 * Usuwa nabój z kontenera
	 * @param bullet
	 */
	private void unregisterExplosion(Explosion explosion)
	{
		explosions.remove(explosion);
	}

	/**
	 * Zwraca iterator do kontenera z graficznymi reprezentacjami obiektów
	 * kafelkowych.
	 * 
	 * @see czolgisci.display.IImageContainer#iterator()
	 */
	public ISpritesIterator iterator()
	{
		return new MapManagerIterator(this);
	}

	/**
	 * Metoda uruchamiana po odebraniu sygnału zegara.
	 * 
	 * @see utils.time.IClockListener#onTick(czolgisci.events.TickEvent)
	 */
	public void onTick(TickEvent e)
	{
		//nie można użyć foreach, bo w pętlach są usuwane
		//i zmieniane obiekty z kolekcji
		for (int i = bullets.size()-1; i >= 0; i--)
		{
			bullets.get(i).onTick(e);
		}
		
		Explosion explosion;
		
		for (int i = explosions.size()-1; i >= 0; i--)
		{
			explosion = explosions.get(i);
			if (explosion.hasExpired())
				unregisterExplosion(explosion);
			else
				explosion.decreaseLifeTime();
		}
	}
	
	/**
	 * @param tile
	 * @return kafelek, na którym znajduje się obiekt
	 */
	private Point getIJfromArea(Rectangle area)
	{
		return getIJfromXY((int) area.getCenterX(), (int) area.getCenterY());
	}
	
	/**
	 * @param x
	 * @param y
	 * @return
	 */
	private Point getIJfromXY(int x, int y)
	{
		int i = (int) Math.ceil((x / tileKit.getTileWidth()));
		int j = (int) Math.ceil((y / tileKit.getTileHeight()));
		return new Point(i, j);
	}
	
	public String toString()
	{
		String str = new String();
		for (int j = 0; j < VerticalTilesNum; j++)
		{
			for (int i = 0; i < HorizontalTilesNum; i++)
			{
				ATile tile = getTileFromIJ(i, j);
				if(tile instanceof Tank)
					str += 'T';
				else if(tile instanceof Wall)
					str += 'W';
				else if(tile instanceof Bullet)
					str += 'B';
				else
					str += ' ';
			}
			str += '\n';
		}
		return str;
	}
}
